﻿namespace MVCResourceEngine
{
    using System;
    using System.Globalization;
    using System.Linq;
    using System.Web;
    using System.Web.Hosting;
    using System.Web.Mvc;
    using System.Web.Routing;

    /// <summary>
    /// The default resource engine.
    /// </summary>
    public class DefaultResourceEngine : IResourceEngine
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DefaultResourceEngine"/> class.
        /// </summary>
        public DefaultResourceEngine()
        {
            // set resource location formats
            this.ResourceLocationFormats = new string[] { "~/Resources/{1}/{0}", "~/Resources/Shared/{0}" };
            
            // set area resource location formats
            this.AreaResourceLocationFormats = new string[] { "~/Areas/{2}/Resources/{1}/{0}", "~/Areas/{2}/Resources/Shared/{0}" };

            // use null view location cache if http context is null or debugging is enabled, 
            // otherwise default view location cache
            if (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled)
            {
                this.ViewLocationCache = DefaultViewLocationCache.Null;
            }
            else
            {
                this.ViewLocationCache = new DefaultViewLocationCache();
            }
        }

        /// <summary>
        /// Gets or sets the area resource location formats.
        /// </summary>
        /// <value>The area resource location formats.</value>
        public string[] AreaResourceLocationFormats { get; set; }

        /// <summary>
        /// Gets or sets the resource location formats.
        /// </summary>
        /// <value>The resource location formats.</value>
        public string[] ResourceLocationFormats { get; set; }

        /// <summary>
        /// Gets or sets the view location cache.
        /// The default resource engine uses the same cache as the view engines.
        /// </summary>
        /// <value>The view location cache.</value>
        public IViewLocationCache ViewLocationCache { get; set; }

        /// <summary>
        /// Finds the resource.
        /// </summary>
        /// <param name="requestContext">The request context.</param>
        /// <param name="resourceName">Name of the resource.</param>
        /// <param name="useCache">true to specify that the resource engine returns the cached resource, if a cached resource exists; otherwise, false</param>
        /// <returns>The resource engine result</returns>
        public ResourceEngineResult FindResource(RequestContext requestContext, string resourceName, bool useCache)
        {
            // throws exception if request context is null
            if (requestContext == null)
            {
                throw new ArgumentNullException("requestContext");
            }

            // throws exception if resource name is null or empty
            if (string.IsNullOrEmpty(resourceName))
            {
                throw new ArgumentException("resourceName");
            }

            // declare searched locations array
            string[] searchedLocations;

            // get resource location
            var resourceLocation = this.GetResourcePath(
                requestContext,
                this.ResourceLocationFormats,
                this.AreaResourceLocationFormats,
                resourceName,
                useCache,
                out searchedLocations);

            // return
            return string.IsNullOrEmpty(resourceLocation) 
                ? new ResourceEngineResult(searchedLocations) 
                : new ResourceEngineResult(resourceLocation, this);
        }

        /// <summary>
        /// Gets the resource path.
        /// </summary>
        /// <param name="requestContext">The request context.</param>
        /// <param name="locationFormats">The location formats.</param>
        /// <param name="areaLocationFormats">The area location formats.</param>
        /// <param name="resourceName">Name of the resource.</param>
        /// <param name="useCache">true to specify that the resource engine returns the cached resource, if a cached resource exists; otherwise, false</param>
        /// <param name="searchedLocations">The searched locations.</param>
        /// <returns>The resource path</returns>
        private string GetResourcePath(RequestContext requestContext, string[] locationFormats, string[] areaLocationFormats, string resourceName, bool useCache, out string[] searchedLocations)
        {
            // get area name
            var areaName = GetAreaName(requestContext.RouteData);

            // get controller name
            var controllerName = requestContext.RouteData.GetRequiredString("controller");

            // set empty searched locations
            searchedLocations = new string[0];

            // generate cache key
            var key = this.CreateCacheKey(resourceName, controllerName, areaName);

            // get resource location from cache if caching is enabled
            if (useCache)
            {
                return this.ViewLocationCache.GetViewLocation(requestContext.HttpContext, key);
            }

            // instanciate array of location formats with or without area location formats
            var resourceLocationFormats = string.IsNullOrEmpty(areaName) ? locationFormats : areaLocationFormats.Union(locationFormats).ToArray();
            
            // instanciate searched locations array
            searchedLocations = new string[resourceLocationFormats.Count()];

            // check each resource location format
            for (var i = 0; i < resourceLocationFormats.Count(); i++)
            {
                // get resource location by resource location format, resource name, controller name and area name
                var resourceLocation = string.Format(CultureInfo.InvariantCulture, resourceLocationFormats[i], resourceName, controllerName, areaName);

                // return resource location if resource file exists
                if (HostingEnvironment.VirtualPathProvider.FileExists(resourceLocation))
                {
                    this.ViewLocationCache.InsertViewLocation(requestContext.HttpContext, key, resourceLocation);
                    return resourceLocation;
                }

                // add searched location to array
                searchedLocations[i] = resourceLocation;
            }

            // return null if no location format fits
            return null;
        }

        /// <summary>
        /// Creates the cache key.
        /// </summary>
        /// <param name="resourceName">Name of the resource.</param>
        /// <param name="controllerName">Name of the controller.</param>
        /// <param name="areaName">Name of the area.</param>
        /// <returns>The cache key.</returns>
        private string CreateCacheKey(string resourceName, string controllerName, string areaName)
        {
            return string.Format(CultureInfo.InvariantCulture, ":ResourceCacheEntry:{0}:{1}:{2}:{3}:", new object[] { this.GetType().AssemblyQualifiedName, resourceName, controllerName, areaName });
        }

        /// <summary>
        /// Gets the name of the area by route data.
        /// </summary>
        /// <param name="routeData">The route data.</param>
        /// <returns>The area name.</returns>
        private static string GetAreaName(RouteData routeData)
        {
            object areaName;

            // try to get area name from data tokens and return it
            if (routeData.DataTokens.TryGetValue("area", out areaName))
            {
                return areaName as string;
            }

            // tray to get area name by route and return it
            return GetAreaName(routeData.Route);
        }

        /// <summary>
        /// Gets the name of the area by route.
        /// </summary>
        /// <param name="route">The route.</param>
        /// <returns>The area name.</returns>
        private static string GetAreaName(RouteBase route)
        {
            // try to get area name from type castet route
            var area = route as IRouteWithArea;
            if (area != null)
            {
                return area.Area;
            }

            // try to get area name from data tokens and return it
            var route2 = route as Route;
            if ((route2 != null) && (route2.DataTokens != null))
            {
                return route2.DataTokens["area"] as string;
            }

            return null;
        }
    }
}
